package com.db.httpserver;

import java.io.EOFException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;

public class NioServer implements Runnable {

	public static void main(String[] args) {
		NioServer server = new NioServer();
		server.run();
	}
	
	public static final int DEFAULT_PORT = 8080;
	private static final int DEFAULT_SELECT_TIMEOUT = 100;

	private AtomicBoolean running = new AtomicBoolean(false);
	private int port;
	private ServerSocketChannel ssc;
	private Selector selector;
	private Logger logger = Logger.getLogger(NioServer.class.getName());

	public NioServer(int port) {
		this.port = port;
	}
	
	public NioServer() {
		this(DEFAULT_PORT);
	}
	
	public boolean isRunning() {
		return running.get();
	}
	
	public boolean stop() {
		return running.compareAndSet(true, false);
	}
	
	@Override
	public void run() {
		
		if (!running.compareAndSet(false, true)) {
			logger.warning("called run(), but server is already running");
			return;
		}
		
		try {
			logger.info("starting server on port " + port);
			selector = Selector.open();
			ssc = ServerSocketChannel.open();
			ssc.configureBlocking(false);
			ssc.socket().bind(new InetSocketAddress(port));
			ssc.register(selector, SelectionKey.OP_ACCEPT);
			logger.info("waiting for connections");
			
			while (running.get()) {
				selector.select(DEFAULT_SELECT_TIMEOUT);
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				for (SelectionKey sk : selectedKeys) {
					try {
						if (sk.isAcceptable())
							accept();
						if (sk.isReadable())
							read(sk);
					} catch (IOException e) {
						unset(sk);
					}
				}
				selectedKeys.clear();
			}
		} catch (IOException e) {
			logger.severe("server failure: " + e.getMessage());
			throw new RuntimeException("Server error", e);
		} finally {
			logger.warning("shutting down the server");
			try {
				selector.close();
				ssc.close();
			} catch (IOException e) {
				logger.severe("failed to shut down properly");
			}
			
			running.set(false);
		}
	}

	private void accept() throws IOException {
		SocketChannel sc = ssc.accept();
		sc.configureBlocking(false);
		SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);
		sk.attach(ByteBuffer.allocate(1024));
		logger.fine("new connection received " + sk);
	}

	private void read(SelectionKey sk) throws IOException {
		SocketChannel channel = (SocketChannel) sk.channel();
		ByteBuffer buffer = (ByteBuffer)sk.attachment();
		
		if (channel.read(buffer) < 0) {
			throw new EOFException("reached EOF");
		}
		
		buffer.flip();
		System.out.println(new String(buffer.array()));
		buffer.clear();
	}

	private void unset(SelectionKey sk) {
		sk.cancel();
		logger.fine("disconnected " + sk);
	}
}
